爆肝整理的Golang面试题,拿走不谢

您所在的位置:网站首页 golang bitmap 爆肝整理的Golang面试题,拿走不谢

爆肝整理的Golang面试题,拿走不谢

2023-05-06 09:49| 来源: 网络整理| 查看: 265

前言1、申明

文章部分题目来源于网络,答案系个人结合5月份面试了近30家公司整理所得,最后附录参考原文链接,如有遗漏的原文出处请联系本人。不对之处望批评指正,答案需要加上自己的思考,最好是代码实践下。

参与过面试的企业有:zg人寿,睿科lun,qi猫,yun汉商城,zi节跳动,特斯la,虾P,chuan音,qi安信,ai立信等大大小小企业近30家,BAT简历都过不了。

2、面试建议

技术部分

1)、算法部分,刷LeetCode就完事了,这是一个长期的过程,短期突击没啥效果,因为题目太多了。

2)、语言基础,细分为:golang基础及原理,就是本文主要内容了;mysql基础及原理;redis基础及原理;kafka或其他消息中间件(如果没用过,需要了解大概的底层原理及结构);linux常用的命令,比如定时脚本几个参数时间分别代表啥,文件权限需要搞清楚,进程内存占用命令;小公司还要懂一些前端的知识,因为他们希望你什么都会。

建议大家可以去牛客网上多刷刷Golong题库,题库总结的很到位,而且题目质量也很高,方便巩固语法基础,还可以去看前端面试题库,题目很全还配有答案,可以方便快速的了解前端知识。

牛客网在线编程_编程学习|练习题_数据结构|系统设计题库

3)、项目经验,可以搞一个基于gin的后端接口服务的web框架,一般会问你怎么实现的;以及微服务了解一下。

非技术部分

1)因为上海5月份居家办公,远程面试,这些题目准备一份,遇到卡壳的题目完全可以参考你准备好的答案,因为视频面试你眼睛是看着面试官还是题目是不太容易区分的(把题目窗口置顶)。

2)HR面也可以完全准备一份可能问到的问题的答案,并不是说你不会回答,而是会让你的表达更顺畅,其次也说明你是有备而来的,我在某拉公司面试就吃了这个亏,技术通过,HR说我的表达能力不行(后续我也会把这个模板分享出来,感谢我媳妇充当面试官,以及指导如何高情商的回答HR的问题)。

3)可以自己录音面试回答,看看自己的语气、音量,顺畅度,如果自己听了都不舒服,面试官可能也不舒服。

一、基础部分1、golang 中 make 和 new 的区别?(基本必问)

共同点:给变量分配内存

不同点:

1)作用变量类型不同,new给string,int和数组分配内存,make给切片,map,channel分配内存;

2)返回类型不一样,new返回指向变量的指针,make返回变量本身;

3)new 分配的空间被清零。make 分配空间后,会进行初始化;

4) 字节的面试官还说了另外一个区别,就是分配的位置,在堆上还是在栈上?这块我比较模糊,大家可以自己探究下,我搜索出来的答案是golang会弱化分配的位置的概念,因为编译的时候会自动内存逃逸处理,懂的大佬帮忙补充下:make、new内存分配是在堆上还是在栈上?

2、数组和切片的区别 (基本必问)

相同点:

1)只能存储一组相同类型的数据结构

2)都是通过下标来访问,并且有容量长度,长度通过 len 获取,容量通过 cap 获取

区别:

1)数组是定长,访问和复制不能超过数组定义的长度,否则就会下标越界,切片长度和容量可以自动扩容

2)数组是值类型,切片是引用类型,每个切片都引用了一个底层数组,切片本身不能存储任何数据,都是这底层数组存储数据,所以修改切片的时候修改的是底层数组中的数据。切片一旦扩容,指向一个新的底层数组,内存地址也就随之改变

简洁的回答:

1)定义方式不一样 2)初始化方式不一样,数组需要指定大小,大小不改变 3)在函数传递中,数组切片都是值传递。

数组的定义

var a1 [3]int

var a2 [...]int{1,2,3}

切片的定义

var a1 []int

var a2 :=make([]int,3,5)

数组的初始化

a1 := [...]int{1,2,3}

a2 := [5]int{1,2,3}

切片的初始化

b:= make([]int,3,5)

3、for range 的时候它的地址会发生变化么?

答:在 for a,b := range c 遍历中, a 和 b 在内存中只会存在一份,即之后每次循环时遍历到的数据都是以值覆盖的方式赋给 a 和 b,a,b 的内存地址始终不变。由于有这个特性,for 循环里面如果开协程,不要直接把 a 或者 b 的地址传给协程。解决办法:在每次循环时,创建一个临时变量。

4、go defer,多个 defer 的顺序,defer 在什么时机会修改返回值?

作用:defer延迟函数,释放资源,收尾工作;如释放锁,关闭文件,关闭链接;捕获panic;

避坑指南:defer函数紧跟在资源打开后面,否则defer可能得不到执行,导致内存泄露。

多个 defer 调用顺序是 LIFO(后入先出),defer后的操作可以理解为压入栈中

defer,return,return value(函数返回值) 执行顺序:首先return,其次return value,最后defer。defer可以修改函数最终返回值,修改时机:有名返回值或者函数返回指针 参考:

【Golang】Go语言defer用法大总结(含return返回机制)__奶酪的博客-CSDN博客

有名返回值

func b() (i int) { defer func() { i++ fmt.Println("defer2:", i) }() defer func() { i++ fmt.Println("defer1:", i) }() return i //或者直接写成return } func main() { fmt.Println("return:", b()) }

////////////////////////////////////////////////////////////////////////////////

函数返回指针

func c() *int { var i int defer func() { i++ fmt.Println("defer2:", i) }() defer func() { i++ fmt.Println("defer1:", i) }() return &i } func main() { fmt.Println("return:", *(c())) }5、uint 类型溢出问题

超过最大存储值如uint8最大是255

var a uint8 =255

var b uint8 =1

a+b = 0总之类型溢出会出现难以意料的事

6、能介绍下 rune 类型吗?

相当int32

golang中的字符串底层实现是通过byte数组的,中文字符在unicode下占2个字节,在utf-8编码下占3个字节,而golang默认编码正好是utf-8

byte 等同于int8,常用来处理ascii字符

rune 等同于int32,常用来处理unicode或utf-8字符

7、 golang 中解析 tag 是怎么实现的?反射原理是什么?(中高级肯定会问,比较难,需要自己多去总结)

参考如下连接

golang中struct关于反射tag_paladinosment的博客-CSDN博客_golang 反射tagtype User struct { name string `json:name-field` age int } func main() { user := &User{"John Doe The Fourth", 20} field, ok := reflect.TypeOf(user).Elem().FieldByName("name") if !ok { panic("Field not found") } fmt.Println(getStructTag(field)) } func getStructTag(f reflect.StructField) string { return string(f.Tag) }

Go 中解析的 tag 是通过反射实现的,反射是指计算机程序在运行时(Run time)可以访问、检测和修改它本身状态或行为的一种能力或动态知道给定数据对象的类型和结构,并有机会修改它。反射将接口变量转换成反射对象 Type 和 Value;反射可以通过反射对象 Value 还原成原先的接口变量;反射可以用来修改一个变量的值,前提是这个值可以被修改;tag是啥:结构体支持标记,name string `json:name-field` 就是 `json:name-field` 这部分

gorm json yaml gRPC protobuf gin.Bind()都是通过反射来实现的

8、调用函数传入结构体时,应该传值还是指针? (Golang 都是传值)

Go 的函数参数传递都是值传递。所谓值传递:指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。参数传递还有引用传递,所谓引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数

因为 Go 里面的 map,slice,chan 是引用类型。变量区分值类型和引用类型。所谓值类型:变量和变量的值存在同一个位置。所谓引用类型:变量和变量的值是不同的位置,变量的值存储的是对值的引用。但并不是 map,slice,chan 的所有的变量在函数内都能被修改,不同数据类型的底层存储结构和实现可能不太一样,情况也就不一样。

9、讲讲 Go 的 slice 底层数据结构和一些特性?

答:Go 的 slice 底层数据结构是由一个 array 指针指向底层数组,len 表示切片长度,cap 表示切片容量。slice 的主要实现是扩容。对于 append 向 slice 添加元素时,假如 slice 容量够用,则追加新元素进去,slice.len++,返回原来的 slice。当原容量不够,则 slice 先扩容,扩容之后 slice 得到新的 slice,将元素追加进新的 slice,slice.len++,返回新的 slice。对于切片的扩容规则:当切片比较小时(容量小于 1024),则采用较大的扩容倍速进行扩容(新的扩容会是原来的 2 倍),避免频繁扩容,从而减少内存分配的次数和数据拷贝的代价。当切片较大的时(原来的 slice 的容量大于或者等于 1024),采用较小的扩容倍速(新的扩容将扩大大于或者等于原来 1.25 倍),主要避免空间浪费,网上其实很多总结的是 1.25 倍,那是在不考虑内存对齐的情况下,实际上还要考虑内存对齐,扩容是大于或者等于 1.25 倍。

(关于刚才问的 slice 为什么传到函数内可能被修改,如果 slice 在函数内没有出现扩容,函数外和函数内 slice 变量指向是同一个数组,则函数内复制的 slice 变量值出现更改,函数外这个 slice 变量值也会被修改。如果 slice 在函数内出现扩容,则函数内变量的值会新生成一个数组(也就是新的 slice,而函数外的 slice 指向的还是原来的 slice,则函数内的修改不会影响函数外的 slice。)

10、讲讲 Go 的 select 底层数据结构和一些特性?(难点,没有项目经常可能说不清,面试一般会问你项目中怎么使用select)

答:go 的 select 为 golang 提供了多路 IO 复用机制,和其他 IO 复用一样,用于检测是否有读写事件是否 ready。linux 的系统 IO 模型有 select,poll,epoll,go 的 select 和 linux 系统 select 非常相似。

select 结构组成主要是由 case 语句和执行的函数组成 select 实现的多路复用是:每个线程或者进程都先到注册和接受的 channel(装置)注册,然后阻塞,然后只有一个线程在运输,当注册的线程和进程准备好数据后,装置会根据注册的信息得到相应的数据。

select 的特性

1)select 操作至少要有一个 case 语句,出现读写 nil 的 channel 该分支会忽略,在 nil 的 channel 上操作则会报错。

2)select 仅支持管道,而且是单协程操作。

3)每个 case 语句仅能处理一个管道,要么读要么写。

4)多个 case 语句的执行顺序是随机的。

5)存在 default 语句,select 将不会阻塞,但是存在 default 会影响性能。

11、讲讲 Go 的 defer 底层数据结构和一些特性?

答:每个 defer 语句都对应一个_defer 实例,多个实例使用指针连接起来形成一个单连表,保存在 gotoutine 数据结构中,每次插入_defer 实例,均插入到链表的头部,函数结束再一次从头部取出,从而形成后进先出的效果。

defer 的规则总结

延迟函数的参数是 defer 语句出现的时候就已经确定了的。

延迟函数执行按照后进先出的顺序执行,即先出现的 defer 最后执行。

延迟函数可能操作主函数的返回值。

申请资源后立即使用 defer 关闭资源是个好习惯。

12、单引号,双引号,反引号的区别?

单引号,表示byte类型或rune类型,对应 uint8和int32类型,默认是 rune 类型。byte用来强调数据是raw data,而不是数字;而rune用来表示Unicode的code point。

双引号,才是字符串,实际上是字符数组。可以用索引号访问某字节,也可以用len()函数来获取字符串所占的字节长度。

反引号,表示字符串字面量,但不支持任何转义序列。字面量 raw literal string 的意思是,你定义时写的啥样,它就啥样,你有换行,它就换行。你写转义字符,它也就展示转义字符。

二、map相关1、map 使用注意的点,是否并发安全?

map的类型是map[key],key类型的ke必须是可比较的,通常情况,会选择内建的基本类型,比如整数、字符串做key的类型。如果要使用struct作为key,要保证struct对象在逻辑上是不可变的。在Go语言中,map[key]函数返回结果可以是一个值,也可以是两个值。map是无序的,如果我们想要保证遍历map时元素有序,可以使用辅助的数据结构,例如orderedmap。

第一,一定要先初始化,否则panic

第二,map类型是容易发生并发访问问题的。不注意就容易发生程序运行时并发读写导致的panic。 Go语言内建的map对象不是线程安全的,并发读写的时候运行时会有检查,遇到并发问题就会导致panic。

2、map 循环是有序的还是无序的?

无序的, map 因扩张⽽重新哈希时,各键值项存储位置都可能会发生改变,顺序自然也没法保证了,所以官方避免大家依赖顺序,直接打乱处理。就是 for range map 在开始处理循环逻辑的时候,就做了随机播种

3、 map 中删除一个 key,它的内存会释放么?(常问)

如果删除的元素是值类型,如int,float,bool,string以及数组和struct,map的内存不会自动释放

如果删除的元素是引用类型,如指针,slice,map,chan等,map的内存会自动释放,但释放的内存是子元素应用类型的内存占用

将map设置为nil后,内存被回收。

这个问题还需要大家去搜索下答案,我记得有不一样的说法,谨慎采用本题答案。

4、怎么处理对 map 进行并发访问?有没有其他方案? 区别是什么?

方式一、使用内置sync.Map,详细参考

https://mbd.baidu.com/ma/s/7Hwd9yMc

方式二、使用读写锁实现并发安全map

https://mbd.baidu.com/ma/s/qO7b0VQU5、 nil map 和空 map 有何不同?

1)可以对未初始化的map进行取值,但取出来的东西是空:

var m1 map[string]string

fmt.Println(m1["1"])

////////////////////////////////////////////////////////////////////////////////

2)不能对未初始化的map进行赋值,这样将会抛出一个异常:

var m1 map[string]string

m1["1"] = "1"

panic: assignment to entry in nil map

////////////////////////////////////////////////////////////////////////////////

3) 通过fmt打印map时,空map和nil map结果是一样的,都为map[]。所以,这个时候别断定map是空还是nil,而应该通过map == nil来判断。

nil map 未初始化,空map是长度为空

6、map 的数据结构是什么?是怎么实现扩容?

答:golang 中 map 是一个 kv 对集合。底层使用 hash table,用链表来解决冲突 ,出现冲突时,不是每一个 key 都申请一个结构通过链表串起来,而是以 bmap 为最小粒度挂载,一个 bmap 可以放 8 个 kv。在哈希函数的选择上,会在程序启动时,检测 cpu 是否支持 aes,如果支持,则使用 aes hash,否则使用 memhash。每个 map 的底层结构是 hmap,是有若干个结构为 bmap 的 bucket 组成的数组。每个 bucket 底层都采用链表结构。

hmap 的结构如下:

type hmap struct { count int // 元素个数 flags uint8 B uint8 // 扩容常量相关字段B是buckets数组的长度的对数 2^B noverflow uint16 // 溢出的bucket个数 hash0 uint32 // hash seed buckets unsafe.Pointer // buckets 数组指针 oldbuckets unsafe.Pointer // 结构扩容的时候用于赋值的buckets数组 nevacuate uintptr // 搬迁进度 extra *mapextra // 用于扩容的指针 }

map 的容量大小

底层调用 makemap 函数,计算得到合适的 B,map 容量最多可容纳 6.52^B 个元素,6.5 为装载因子阈值常量。装载因子的计算公式是:装载因子=填入表中的元素个数/散列表的长度,装载因子越大,说明空闲位置越少,冲突越多,散列表的性能会下降。底层调用 makemap 函数,计算得到合适的 B,map 容量最多可容纳 6.52^B 个元素,6.5 为装载因子阈值常量。装载因子的计算公式是:装载因子=填入表中的元素个数/散列表的长度,装载因子越大,说明空闲位置越少,冲突越多,散列表的性能会下降。

触发 map 扩容的条件

1)装载因子超过阈值,源码里定义的阈值是 6.5。

2)overflow 的 bucket 数量过多 map 的 bucket 定位和 key 的定位高八位用于定位 bucket,低八位用于定位 key,快速试错后再进行完整对比

7、slices能作为map类型的key吗?

当时被问的一脸懵逼,其实是这个问题的变种:golang 哪些类型可以作为map key?

答案是:在golang规范中,可比较的类型都可以作为map key;这个问题又延伸到在:golang规范中,哪些数据类型可以比较?

不能作为map key 的类型包括:

slicesmapsfunctions

详细参考:

golang 哪些类型可以作为map key三、context相关1、context 结构是什么样的?context 使用场景和用途?

(难,也常常问你项目中怎么用,光靠记答案很难让面试官满意,反正有各种结合实际的问题)

参考链接:

go context详解 - 卷毛狒狒 - 博客园

答:Go 的 Context 的数据结构包含 Deadline,Done,Err,Value,Deadline 方法返回一个 time.Time,表示当前 Context 应该结束的时间,ok 则表示有结束时间,Done 方法当 Context 被取消或者超时时候返回的一个 close 的 channel,告诉给 context 相关的函数要停止当前工作然后返回了,Err 表示 context 被取消的原因,Value 方法表示 context 实现共享数据存储的地方,是协程安全的。context 在业务中是经常被使用的,

其主要的应用 :

1:上下文控制,2:多个 goroutine 之间的数据交互等,3:超时控制:到某个时间点超时,过多久超时。

四、channel相关1、channel 是否线程安全?锁用在什么地方?

就着图片里面的答案看看吧。

2、go channel 的底层实现原理 (数据结构)

底层结构需要描述出来,这个简单,buf,发送队列,接收队列,lock。

3、nil、关闭的 channel、有数据的 channel,再进行读、写、关闭会怎么样?(各类变种题型,重要)

还要去了解一下单向channel,如只读或者只写通道常见的异常问题,这块还需要大家自己总结总结,有懂得大佬也可以评论发送答案。

4、向 channel 发送数据和从 channel 读数据的流程是什么样的?

发送流程:

接收流程:

这个没啥好说的,底层原理,1、2、3描述出来,保证面试官满意。具体的文字描述下面一题有,channel的概念多且复杂,脑海中有个总分的概念,否则你说的再多,面试官也抓不住你说的重点,等于白说。问题5已经为大家总结好了。

5、讲讲 Go 的 chan 底层数据结构和主要使用场景

答:channel 的数据结构包含 qccount 当前队列中剩余元素个数,dataqsiz 环形队列长度,即可以存放的元素个数,buf 环形队列指针,elemsize 每个元素的大小,closed 标识关闭状态,elemtype 元素类型,sendx 队列下表,指示元素写入时存放到队列中的位置,recv 队列下表,指示元素从队列的该位置读出。recvq 等待读消息的 goroutine 队列,sendq 等待写消息的 goroutine 队列,lock 互斥锁,chan 不允许并发读写。

无缓冲和有缓冲区别: 管道没有缓冲区,从管道读数据会阻塞,直到有协程向管道中写入数据。同样,向管道写入数据也会阻塞,直到有协程从管道读取数据。管道有缓冲区但缓冲区没有数据,从管道读取数据也会阻塞,直到协程写入数据,如果管道满了,写数据也会阻塞,直到协程从缓冲区读取数据。

channel 的一些特点 1)、读写值 nil 管道会永久阻塞 2)、关闭的管道读数据仍然可以读数据 3)、往关闭的管道写数据会 panic 4)、关闭为 nil 的管道 panic 5)、关闭已经关闭的管道 panic

向 channel 写数据的流程: 如果等待接收队列 recvq 不为空,说明缓冲区中没有数据或者没有缓冲区,此时直接从 recvq 取出 G,并把数据写入,最后把该 G 唤醒,结束发送过程; 如果缓冲区中有空余位置,将数据写入缓冲区,结束发送过程; 如果缓冲区中没有空余位置,将待发送数据写入 G,将当前 G 加入 sendq,进入睡眠,等待被读 goroutine 唤醒;

向 channel 读数据的流程: 如果等待发送队列 sendq 不为空,且没有缓冲区,直接从 sendq 中取出 G,把 G 中数据读出,最后把 G 唤醒,结束读取过程; 如果等待发送队列 sendq 不为空,此时说明缓冲区已满,从缓冲区中首部读出数据,把 G 中数据写入缓冲区尾部,把 G 唤醒,结束读取过程; 如果缓冲区中有数据,则从缓冲区取出数据,结束读取过程;将当前 goroutine 加入 recvq,进入睡眠,等待被写 goroutine 唤醒;

使用场景: 消息传递、消息过滤,信号广播,事件订阅与广播,请求、响应转发,任务分发,结果汇总,并发控制,限流,同步与异步

五、GMP相关1、什么是 GMP?(必问)

答:G 代表着 goroutine,P 代表着上下文处理器,M 代表 thread 线程,在 GPM 模型,有一个全局队列(Global Queue):存放等待运行的 G,还有一个 P 的本地队列:也是存放等待运行的 G,但数量有限,不超过 256 个。GPM 的调度流程从 go func()开始创建一个 goroutine,新建的 goroutine 优先保存在 P 的本地队列中,如果 P 的本地队列已经满了,则会保存到全局队列中。M 会从 P 的队列中取一个可执行状态的 G 来执行,如果 P 的本地队列为空,就会从其他的 MP 组合偷取一个可执行的 G 来执行,当 M 执行某一个 G 时候发生系统调用或者阻塞,M 阻塞,如果这个时候 G 在执行,runtime 会把这个线程 M 从 P 中摘除,然后创建一个新的操作系统线程来服务于这个 P,当 M 系统调用结束时,这个 G 会尝试获取一个空闲的 P 来执行,并放入到这个 P 的本地队列,如果这个线程 M 变成休眠状态,加入到空闲线程中,然后整个 G 就会被放入到全局队列中。

关于 G,P,M 的个数问题,G 的个数理论上是无限制的,但是受内存限制,P 的数量一般建议是逻辑 CPU 数量的 2 倍,M 的数据默认启动的时候是 10000,内核很难支持这么多线程数,所以整个限制客户忽略,M 一般不做设置,设置好 P,M 一般都是要大于 P。

2、进程、线程、协程有什么区别?(必问)

进程:是应用程序的启动实例,每个进程都有独立的内存空间,不同的进程通过进程间的通信方式来通信。

线程:从属于进程,每个进程至少包含一个线程,线程是 CPU 调度的基本单位,多个线程之间可以共享进程的资源并通过共享内存等线程间的通信方式来通信。

协程:为轻量级线程,与线程相比,协程不受操作系统的调度,协程的调度器由用户应用程序提供,协程调度器按照调度策略把协程调度到线程中运行

3、抢占式调度是如何抢占的?

基于协作式抢占

基于信号量抢占

就像操作系统要负责线程的调度一样,Go的runtime要负责goroutine的调度。现代操作系统调度线程都是抢占式的,我们不能依赖用户代码主动让出CPU,或者因为IO、锁等待而让出,这样会造成调度的不公平。基于经典的时间片算法,当线程的时间片用完之后,会被时钟中断给打断,调度器会将当前线程的执行上下文进行保存,然后恢复下一个线程的上下文,分配新的时间片令其开始执行。这种抢占对于线程本身是无感知的,系统底层支持,不需要开发人员特殊处理。

基于时间片的抢占式调度有个明显的优点,能够避免CPU资源持续被少数线程占用,从而使其他线程长时间处于饥饿状态。goroutine的调度器也用到了时间片算法,但是和操作系统的线程调度还是有些区别的,因为整个Go程序都是运行在用户态的,所以不能像操作系统那样利用时钟中断来打断运行中的goroutine。也得益于完全在用户态实现,goroutine的调度切换更加轻量。

上面这两段文字只是对调度的一个概括,具体的协作式调度、信号量调度大家还需要去详细了解,这偏底层了,大厂或者中高级开发会问。(字节就问了)

4、M 和 P 的数量问题?

p默认cpu内核数

M与P的数量没有绝对关系,一个M阻塞,P就会去创建或者切换另一个M,所以,即使P的默认数量是1,也有可能会创建很多个M出来

【Go语言调度模型G、M、P的数量多少合适?】

详细参考这篇文章

https://mbd.baidu.com/ma/s/ZMOKQATr

GMP数量这一块,结论很好记,没用项目经验的话,问了项目中怎么用可能容易卡壳。

六、锁相关1、除了 mutex 以外还有那些方式安全读写共享变量?

* 将共享变量的读写放到一个 goroutine 中,其它 goroutine 通过 channel 进行读写操作。

* 可以用个数为 1 的信号量(semaphore)实现互斥

* 通过 Mutex 锁实现

2、Go 如何实现原子操作?

答:原子操作就是不可中断的操作,外界是看不到原子操作的中间状态,要么看到原子操作已经完成,要么看到原子操作已经结束。在某个值的原子操作执行的过程中,CPU 绝对不会再去执行其他针对该值的操作,那么其他操作也是原子操作。

Go 语言的标准库代码包 sync/atomic 提供了原子的读取(Load 为前缀的函数)或写入(Store 为前缀的函数)某个值(这里细节还要多去查查资料)。

原子操作与互斥锁的区别

1)、互斥锁是一种数据结构,用来让一个线程执行程序的关键部分,完成互斥的多个操作。

2)、原子操作是针对某个值的单个互斥操作。

3、Mutex 是悲观锁还是乐观锁?悲观锁、乐观锁是什么?

悲观锁

悲观锁:当要对数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发。这种借助数据库锁机制,在修改数据之前先锁定,再修改的方式被称之为悲观并发控制【Pessimistic Concurrency Control,缩写“PCC”,又名“悲观锁”】。

乐观锁

乐观锁是相对悲观锁而言的,乐观锁假设数据一般情况不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果冲突,则返回给用户异常信息,让用户决定如何去做。乐观锁适用于读多写少的场景,这样可以提高程序的吞吐量

4、Mutex 有几种模式?

1)正常模式

当前的mutex只有一个goruntine来获取,那么没有竞争,直接返回。新的goruntine进来,如果当前mutex已经被获取了,则该goruntine进入一个先入先出的waiter队列,在mutex被释放后,waiter按照先进先出的方式获取锁。该goruntine会处于自旋状态(不挂起,继续占有cpu)。新的goruntine进来,mutex处于空闲状态,将参与竞争。新来的 goroutine 有先天的优势,它们正在 CPU 中运行,可能它们的数量还不少,所以,在高并发情况下,被唤醒的 waiter 可能比较悲剧地获取不到锁,这时,它会被插入到队列的前面。如果 waiter 获取不到锁的时间超过阈值 1 毫秒,那么,这个 Mutex 就进入到了饥饿模式。

2)饥饿模式

在饥饿模式下,Mutex 的拥有者将直接把锁交给队列最前面的 waiter。新来的 goroutine 不会尝试获取锁,即使看起来锁没有被持有,它也不会去抢,也不会 spin(自旋),它会乖乖地加入到等待队列的尾部。 如果拥有 Mutex 的 waiter 发现下面两种情况的其中之一,它就会把这个 Mutex 转换成正常模式:

此 waiter 已经是队列中的最后一个 waiter 了,没有其它的等待锁的 goroutine 了;此 waiter 的等待时间小于 1 毫秒。5、goroutine 的自旋占用资源如何解决

自旋锁是指当一个线程在获取锁的时候,如果锁已经被其他线程获取,那么该线程将循环等待,然后不断地判断是否能够被成功获取,直到获取到锁才会退出循环。

自旋的条件如下:

1)还没自旋超过 4 次,

2)多核处理器,

3)GOMAXPROCS > 1,

4)p 上本地 goroutine 队列为空。

mutex 会让当前的 goroutine 去空转 CPU,在空转完后再次调用 CAS 方法去尝试性的占有锁资源,直到不满足自旋条件,则最终会加入到等待队列里。

七、并发相关1、怎么控制并发数?

第一,有缓冲通道

根据通道中没有数据时读取操作陷入阻塞和通道已满时继续写入操作陷入阻塞的特性,正好实现控制并发数量。

func main() { count := 10 // 最大支持并发 sum := 100 // 任务总数 wg := sync.WaitGroup{} //控制主协程等待所有子协程执行完之后再退出。 c := make(chan struct{}, count) // 控制任务并发的chan defer close(c) for i:=0; i


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3